<?php
/**
 * This file defines the class {@see \BF\IO\File}
 *
 * @author     Kado <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @package    BF\IO
 * @since      2015-01-31 00:29
 * @subpackage Core
 * @version    0.1
 */

namespace BF\IO
{

   /**
    * Diese Klasse definiert ein Objekt zur einfacheren gekapslten Handhabung
    * von Dateizugriffen. Eine Instanz kann nur über die statischen Methoden
    * {@see \BF\IO\File::CreateNew} und {@see \BF\IO\File::OpenRead}
    * erstellt werden.
    *
    * Ansonsten ist eine Nutzung der zahlreichen staischen Hilfsmethoden
    * angedacht.
    *
    * Mit 2 Methoden können Dateien gezipt werden und mit einer kann eine
    * ZIP-Archivdatei entpackt werden.
    *
    * @since      v0.1
    */
   class File
   {

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R I V A T E   F I E L D S   - - - - - - - - - - - - - - - - - - - - - -">

      private $file;

      private $access;

      private $fp;

      private $mode;

      private $locked;

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   C O N S T A N T S   - - - - - - - - - - - - - - - - - - - - - - - - - - -">

      /**
       * Reading file access.
       */
      const ACCESS_READ = 'read';

      /**
       * Writing file access.
       */
      const ACCESS_WRITE = 'write';

      /**
       * Reading and writing file access.
       */
      const ACCESS_READWRITE = 'read and write';

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - >   P R I V A T E   C O N S T R U C T O R   - - - - - - - - - - - - - - - - -">

      /**
       * Inits a new instance.
       *
       * @param string   $file
       * @param string   $access
       * @param resource $fp
       * @param integer  $mode
       * @param boolean  $locked
       */
      private function __construct( $file, $access, $fp, $mode, $locked )
      {
          $this->access = $access;
          $this->file = $file;
          $this->fp = $fp;
          $this->mode = $mode;
          $this->locked = $locked;
      }

      /**
       * The destructor.
       */
      public function  __destruct()
      {
          $this->close();
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   M E T H O D S   - - - - - - - - - - - - - - - - - - - - - -">

      # <editor-fold defaultstate="collapsed" desc="BOOLEAN METHODS">

      /**
       * Returns, if currently a usable file resource is open.
       *
       * @return boolean
       */
      public function isOpen()
      {
         return \is_resource($this->fp);
      }

      /**
       * Returns if reading is enabled.
       *
       * @return boolean
       */
      public function hasReadAccess()
      {
         return $this->isOpen() && ( $this->access == self::ACCESS_READ || $this->access == self::ACCESS_READWRITE );
      }

      /**
       * Returns if writing is enabled.
       *
       * @return boolean
       */
      public function hasWriteAccess()
      {
         return $this->isOpen() && ( $this->access == self::ACCESS_WRITE || $this->access == self::ACCESS_READWRITE );
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="READING METHODS">

      /**
       * Reads the next line and returns it.
       *
       * @param  boolean $removeNewlines    Remove trailing linebreaks? (Defaults to TRUE)
       * @param  boolean $fast              Read fast without some checks? (Defaults to FALSE)
       * @return FALSE|string               Returns the resulting line, or (boolean)FASLE if EOF is reached.
       * @throws \BF\IO\FileAccessException If reading is not allowed or if it fails.
       */
      public function readLine( $removeNewlines = true, $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasReadAccess() )
            {
               throw \BF\IO\FileAccessException::Read(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not read!', $this->access )
               );
            }
         }
         try
         {
            if ( \feof( $this->fp ) )
            {
               return false;
            }
            if ( ! $removeNewlines )
            {
               return \fgets( $this->fp );
            }
            return \rtrim( \fgets( $this->fp ), "\r\n" );
         }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_READ,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Reads the next $count bytes and returns it.
       *
       * @param  integer $count             The count of bytes to read (Defaults to 1)
       * @param  boolean $fast              Read fast without some checks? (Defaults to FALSE)
       * @return FALSE|string               Returns the resulting string, or (boolean)FALSE if EOF is reached.
       * @throws \BF\IO\FileAccessException If reading is not allowed or if it fails.
       */
      public function read( $count = 1, $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( !$this->hasReadAccess())
            {
               throw \BF\IO\FileAccessException::Read(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not read!', $this->access )
               );
            }
         }
         try
         {
            if ( \feof( $this->fp ) )
            {
               return false;
            }
            return \fread( $this->fp, $count );
         }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_READ,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Reads the next char and returns it.
       *
       * @param  boolean $fast              Read fast without some checks? (Defaults to FALSE)
       * @return FALSE|string               Returns the resulting char, or (boolean)FALSE if EOF is reached.
       * @throws \BF\IO\FileAccessException If reading is not allowed or if it fails.
       */
      public function readChar( $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasReadAccess() )
            {
               throw \BF\IO\FileAccessException::Read(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not read!', $this->access )
               );
            }
         }
         try
         {
            if ( \feof( $this->fp ) )
            {
               return false;
            }
            return \fgetc( $this->fp );
         }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_READ,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Reads a CSV row and returns it.
       *
       * @param  char    $delimiter         The CSV element separator (Defaults to ',')
       * @param  integer $maxLineLength     The max allowed line length (Defaults to 1024)
       * @param  boolean $fast              Read fast without some checks? (Defaults to FALSE)
       * @return array|FALSE                Returns the resulting row, or (boolean)FALSE if EOF is reached.
       * @throws \BF\IO\FileAccessException If reading is not allowed or if it fails.
       */
      public function readCsv( $delimiter = ',', $maxLineLength = 1024, $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasReadAccess() )
            {
               throw \BF\IO\FileAccessException::Read(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not read!', $this->access )
               );
            }
         }
         try
         {
            return \fgetcsv( $this->fp, $maxLineLength, $delimiter );
         }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_READ,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Reads all from current pointer to EOF.
       *
       * @param  boolean $getLines          Return the result als lines array? (Default=FALSE)
       * @param  boolean $removeNewlines    Remove line breaks if $getLines is TRUE? (Default=TRUE)
       * @param  boolean $fast              Read fast without some checks? (Defaults to FALSE)
       * @return array|string|FALSE
       * @throws \BF\IO\FileAccessException If reading is not allowed or if it fails.
       */
      public function readToEnd( $getLines = false, $removeNewlines = true, $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasReadAccess() )
            {
               throw \BF\IO\FileAccessException::Read(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not read!', $this->access )
               );
            }
         }
         $result = '';
         if ( $getLines )
         {
            $result = array();
         }
         while ( false !== ( $line = $this->readLine( $removeNewlines && $getLines, true ) ) )
         {
            if ( $getLines )
            {
               $result[] = $line;
            }
            else
            {
               $result .= $line;
            }
         }
         return $result;
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="WRITING METHODS">

      /**
       * Writes a a string to current file, with a trailing linebreak.
       *
       * @param  string  $str               The string to write.
       * @param  string  $newline           Use this linebreak (Default=\n)
       * @param  boolean $fast              Write fast without some checks? (Defaults to FALSE)
       * @return boolean
       * @throws \BF\IO\FileAccessException If writing is not allowed or if it fails.
       */
      public function writeLine( $str, $newline = "\n", $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasWriteAccess() )
            {
               throw \BF\IO\FileAccessException::Write(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not write!', $this->access )
               );
            }
         }
         try { \fwrite( $this->fp, $str . $newline ); }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_WRITE,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
         return true;
      }

      /**
       * Writes a a string to current file, with a trailing linebreak. (Some checks are disabled)
       *
       * @param  string  $str               The string to write.
       * @param  string  $newline           Use this linebreak (Default=\n)
       * @throws \BF\IO\FileAccessException If writing is not allowed or if it fails.
       */
      public function writeLineFast( $str, $newline = "\n" )
      {
         $this->writeLine( $str, $newline, true );
      }

      /**
       * Writes a a string or lines array to current file.
       *
       * @param  string|array $strOrArray   The string to write, or a lines array to write.
       * @param  string  $newline           Use this linebreak (Default=\n)
       * @param  boolean $fast              Write fast without some checks? (Defaults to FALSE)
       * @throws \BF\IO\FileAccessException If writing is not allowed or if it fails.
       */
      public function write( $strOrArray, $newline = "\n", $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasWriteAccess() )
            {
               throw \BF\IO\FileAccessException::Write(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not write!', $this->access )
               );
            }
         }
         try
         {
            if ( \is_array( $strOrArray ) )
            {
                foreach ( $strOrArray as $line )
                {
                    \fwrite( $this->fp, \rtrim($line, "\r\n") . $newline );
                }
            }
            else { \fwrite( $this->fp, $strOrArray ); }
         }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_WRITE,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Writes a a string or lines array to current file.
       *
       * @param  string  $chars             The string to write, or a lines array to write.
       * @param  boolean $fast              Write fast without some checks? (Defaults to FALSE)
       * @throws \BF\IO\FileAccessException If writing is not allowed or if it fails.
       */
      public function writeChars( $chars, $fast = true )
      {
         $this->write( $chars, '', $fast );
      }

      /**
       * Write a csv format data row, defined as array.
       *
       * @param  array   $dataRow           The datarow to write (numeric indicated array)
       * @param  char    $delimiter         The CSV column delimiter char. (default=',')
       * @param  boolean $fast              Write fast without some checks? (Defaults to FALSE)
       * @throws \BF\IO\FileAccessException If writing is not allowed or if it fails.
       */
      public function writeCsv( array $dataRow, $delimiter = ',', $fast = false )
      {
         if ( ! $fast )
         {
            if ( ! $this->isOpen() )
            {
               return false;
            }
            if ( ! $this->hasWriteAccess() )
            {
               throw \BF\IO\FileAccessException::Write(
                  $this->file,
                  \sprintf( 'Current mode of opened file is "%s" and not write!', $this->access )
               );
            }
         }
         try { \fputcsv( $this->fp, $dataRow, $delimiter ); }
         catch ( \BF\PhpException $ex )
         {
            throw new \BF\IO\FileAccessException(
               $this->file,
               \BF\IO\FileAccessException::ACCESS_WRITE,
               'Writing of CSV-Data fails.',
               \E_USER_ERROR,
               $ex
            );
         }
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="PointerPosition">

      /**
       * Returns the position of the current file pointer.
       *
       * @return integer|FALSE If there is no usable pointer, FALSE is returned
       */
      public function getPointerPosition()
      {
         if ( ! \is_resource( $this->fp ) )
         {
            return false;
         }
         return \ftell( $this->fp );
      }

      /**
       * Sets a new file pointer position.
       *
       * @param  integer $offset
       * @return boolean
       */
      public function setPointerPosition( $offset = 0 )
      {
         if ( ! \is_resource( $this->fp ) )
         {
            return false;
         }
         return (bool) \fseek( $this->fp, $offset );
      }

      /**
       * Sets the file pointer position to the end of the file.
       *
       * @return boolean
       */
      public function setPointerPositionToEndOfFile()
      {
         if ( ! \is_resource( $this->fp ) )
         {
            return false;
         }
         return (bool) \fseek( $this->fp, 0, \SEEK_END );
      }

      # </editor-fold>

      /**
       * Closes the current file pointer.
       */
      public function close()
      {
         if ( ! \is_resource( $this->fp ) )
         {
            return;
         }
         \fclose( $this->fp );
         $this->fp = null;
         if ( $this->access == self::ACCESS_WRITE || $this->access == self::ACCESS_READWRITE )
         {
            if ( ! \is_null( $this->mode ) && ! \IS_WIN )
            {
               \chmod( $this->file, $this->mode );
            }
         }
      }

      /**
       * Returns the current used file path.
       *
       * @return string
       */
      public final function getFileName()
      {
         return $this->file;
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - - -">

      # <editor-fold defaultstate="collapsed" desc="P U B L I C   S T A T I C   I N S T A N C E   M E T H O D S">

      /**
       * Creates a new file and returns the associated {@see \BF\IO\File} instance.
       *
       * If $overwrite is FALSE and the file already exists, a {@see \BF\IO\FileAlreadyExistsException} is thrown.
       *
       * @param  string  $file                     The path of the file to create.
       * @param  boolean $overwrite                Overwrite the file if it exists? (default=TRUE)
       * @param  boolean $lock                     Use file locking? (default=FALSE)
       * @param  boolean $lockExclusive            If $lock is TRUE, use exclusive file locking? (default=FALSE)
       * @param  integer $mode                     The file access mode (only used by unixoids) (default=0755)
       * @return \BF\IO\File                       Returns the newly created File instance
       * @throws \BF\IO\FileAlreadyExistsException If file exists and overwriting is disabled.
       * @throws \BF\IO\FileAccessException        On errors while opening the file pointer
       */
      public static function CreateNew (
          $file, $overwrite = true, $lock = false, $lockExclusive = false, $mode = 0755 )
      {
         if ( \file_exists( $file ) )
         {
            if ( ! $overwrite )
            {
               throw new \BF\IO\FileAlreadyExistsException(
                  $file,
                  'Creation of this file fails!'
               );
            }
         }
         try
         {
            $fp = \fopen( $file, 'wb' );
            $locked = false;
            if ( $lock )
            {
               $locked = \flock( $fp, $lockExclusive ? \LOCK_EX : \LOCK_SH );
            }
            return new \BF\IO\File( $file, self::ACCESS_WRITE, $fp, $mode, $locked );
         }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\FileAccessException(
               $file,
               \BF\IO\FileAccessException::ACCESS_CREATE,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Opens a existing file for writing and sets the file pointer to the end of the file and
       * returns the associated {@see \BF\IO\File} instance. If the file does not exist it will be created.
       *
       * @param  string  $file                     The path of the file to open or create.
       * @param  boolean $lock                     Use file locking? (default=FALSE)
       * @param  boolean $lockExclusive            If $lock is TRUE, use exclusive file locking? (default=FALSE)
       * @param  integer $mode                     The file access mode (only used with unixoids) (default=0755)
       * @return \BF\IO\File                       Returns the File instance
       * @throws \BF\IO\FileAccessException        On errors while opening the file pointer
       */
      public static function OpenForAppend ( $file, $lock = false, $lockExclusive = false, $mode = 0755 )
      {
         if ( ! \file_exists( $file ) )
         {
            return self::CreateNew( $file, true, $lock, $lockExclusive, $mode );
         }
         try
         {
            $fp = \fopen( $file, 'ab' );
            $locked = false;
            if ( $lock )
            {
               $locked = \flock( $fp, $lockExclusive ? \LOCK_EX : \LOCK_SH );
            }
            return new \BF\IO\File( $file, self::ACCESS_WRITE, $fp, $mode, $locked );
         }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\FileAccessException(
               $file,
               \BF\IO\FileAccessException::ACCESS_CREATE,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Opens a existing file for reading and returns the associated {@see \BF\IO\File} instance.
       *
       * @param  string  $file                The path of the file to read.
       * @param  boolean $lock                Use file locking? (default=FALSE)
       * @param  boolean $lockExclusive       If $lock is TRUE, use exclusive file locking? (default=FALSE)
       * @return \BF\IO\File                  Returns the newly created File instance
       * @throws \BF\IO\FileNotFoundException If the file not exists
       * @throws \BF\IO\FileAccessException   On errors while opening the file pointer
       */
      public static function OpenRead( $file, $lock = false, $lockExclusive = false )
      {
         if ( ! \file_exists( $file ) )
         {
            throw new \BF\IO\FileNotFoundException(
               $file,
               'Open file for reading fails.'
            );
         }
         try
         {
            $fp = \fopen( $file, 'rb' );
            $locked = false;
            if ( $lock )
            {
               $locked = \flock( $fp, $lockExclusive ? \LOCK_EX : \LOCK_SH );
            }
            return new \BF\IO\File( $file, File::ACCESS_READ, $fp, 0750, $locked );
         }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\FileAccessException(
               $file,
               \BF\IO\FileAccessException::ACCESS_READ,
               null,
               \E_USER_ERROR,
               $ex
            );
         }
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="Create + Delete + Move + Copy + ReadFirstBytes">

      /**
       * Creates a new file with the defined content.
       *
       * If $overwrite is FALSE and the file already exists, a {@see \BF\IO\FileAlreadyExistsException} is thrown.
       *
       * @param  string       $file                The path of the file to create.
       * @param  integer      $mode                The file access mode (only used by unixoids) (default=0750)
       * @param  boolean      $overwrite           Overwrite the file if it exists? (default=TRUE)
       * @param  string|array $contents            The contents of the file (string or lines array) (default='')
       * @throws \BF\IO\FileAlreadyExistsException If file exists and overwriting is disabled.
       * @throws \BF\IO\FileAccessException        On errors while opening the file pointer
       */
      public static function Create( $file, $mode = 0750, $overwrite = true, $contents = '' )
      {
         $f = \BF\IO\File::CreateNew( $file, $overwrite, true, true, $mode );
         $f->write( $contents );
         $f->close();
      }

      /**
       * Deletes the defined file.
       *
       * @param  string $file THe path of the file to delete
       * @throws \BF\IO\Exception
       */
      public static function Delete( $file )
      {
         if ( ! \file_exists( $file ) )
         {
            // No deleting required
            return;
         }
         try
         {
            \unlink( $file );
            \clearstatcache();
            if ( ! \file_exists( $file ) )
            {
               // Deleting was successfull
               return;
            }
         }
         catch ( \BF\PhpException $ex ) { }
         if ( \IS_WIN )
         {
            $f = \escapeshellarg( $file );
            try { \exec( "del {$f}" ); }
            catch ( \Exception $ex )
            {
               throw new \BF\IO\Exception( $file, 'Deleting the defined file fails!', \E_USER_ERROR, $ex );
            }
            \clearstatcache();
            if ( \file_exists( $file ) )
            {
               throw new \BF\IO\Exception(
                  $file,
                  'Deleting the defined file fails! No known method works.'
               );
            }
            return;
         }
         try { \exec( "unlink {$file}" ); }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\Exception( $file, 'Deleting the defined file fails!', \E_USER_ERROR, $ex );
         }
         \clearstatcache();
         if ( \file_exists( $file ) )
         {
            throw new \BF\IO\Exception(
               $file,
               'Deleting the defined file fails!'
            );
         }
      }

      /**
       * Moves the defined file to a new location $targetFile. (Also known as renaming ;-) )
       *
       * If $replace is FALSE it only does the job if the $targetFile not exists, otherwise a
       * {@see \BF\IO\FileAlreadyExistsException} is trown.
       *
       * @param  string $srcFile                   The file to move.
       * @param  string $targetFile                The target file path.
       * @param  bool   $replace                   Replace the target if it exists? (default=true)
       * @throws \BF\IO\FileNotFoundException      If the $srcFile does not exist.
       * @throws \BF\IO\FileAlreadyExistsException If $targetFile exists and overwriting is disabled.
       * @throws \BF\IO\Exception
       */
      public static function Move( $srcFile, $targetFile, $replace = true )
      {
         self::Copy( $srcFile, $targetFile, $replace );
         self::Delete( $srcFile );
      }

      /**
       * Copies the source file to the target file. $targetFile will only be overwritten if $overwrite is TRUE,
       * otherwise a {@see \BF\IO\FileAlreadyExistsException} is thrown.
       *
       * @param  string $sourceFile                The source file
       * @param  string $targetFile                The target/destination file.
       * @param  bool   $overwrite                 Overwrite $targetFile if it exists? (default=TRUE)
       * @throws \BF\IO\FileNotFoundException      If the $srcFile does not exist.
       * @throws \BF\IO\FileAlreadyExistsException If $targetFile exists and overwriting is disabled.
       * @throws \BF\IO\Exception
       */
      public static function Copy( $sourceFile, $targetFile, $overwrite = true )
      {
         if ( !\file_exists($sourceFile) )
         {
            throw new \BF\IO\FileNotFoundException(
               $sourceFile,
               'Could not copy a dont existing file.'
            );
         }
         if ( \file_exists( $targetFile ) )
         {
            if ( ! $overwrite )
            {
               throw new \BF\IO\FileAlreadyExistsException(
                  $targetFile,
                  'Could not copy a file to defined target-file if overwriting is not allowed by current call of File::Copy()'
               );
            }
            self::Delete( $targetFile );
         }
         $output = null;
         try
         {
            $res = \copy( $sourceFile, $targetFile );
            if ( FALSE === $res )
            {
               throw new \Exception();
            }
         }
         catch ( \Exception $ex )
         {
            $ex = null;
            try
            {
               $return_var = 1;
               if ( \IS_WIN )
               {
                  \ob_start();
                  \exec(
                     "copy /Y /B \"{$sourceFile}\" \"{$targetFile}\" 2>&1",
                     $output,
                     $return_var
                  );
                  \ob_end_clean();
               }
               else
               {
                  \ob_start();
                  \exec(
                     "cp {$sourceFile} {$targetFile} 2>&1",
                     $output,
                     $return_var
                  );
                  \ob_end_clean();
               }
               if ( 0 !== $return_var )
               {
                  throw new \BF\IO\Exception( $sourceFile, $output[ 0 ] );
               }
            }
            catch ( \Exception $ex1 )
            {
               throw new \BF\IO\Exception(
                   $sourceFile,
                   \sprintf( 'Copying file to "%s" fails.', $targetFile ),
                   $ex1
               );
            }
         }
      }

      /**
       * Read the first $count bytes.
       *
       * @param  string  $file
       * @param  integer $count
       * @return string
       */
      public static function ReadFirstBytes( $file, $count )
      {
         try
         {
            $f = self::OpenRead( $file );
            $res = $f->read( $count );
            $f->close();
            return $res;
         }
         catch ( \Exception $ex ) { $ex = null; }
         return '';
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="Zip + ZipList  + UnZip">

      /**
       * Creates a new ZIP archive from a single file.
       *
       * @param  string $sourceFile This file will be compressed by the zip archive
       * @param  string $zipFile    The target/destination ZIP file. (will be created or overwrite a existing)
       * @param  string $workingdir A optional working folder. (Usual its the folder, containing the $sourceFile)
       * @throws \BF\MissingExtensionException If the PHP ZIP extension is not loaded.
       * @throws \BF\IO\FileAccessException If creating the ZIP file fails.
       * @throws \BF\IO\Exception
       */
      public static function Zip( $sourceFile, $zipFile, $workingdir = null )
      {
         if ( ! \class_exists( '\\ZipArchive' ) )
         {
            throw new \BF\MissingExtensionException(
               'Zip',
               '\BF\IO\File::Zip() fails with no ZIP-Support.'
            );
         }
         $owd = \BF\IO\Path::Unixize( \getcwd() );
         if ( empty( $workingdir ) )
         {
            $workingdir = \BF\IO\Path::Unixize( \dirname( $sourceFile ) );
         }
         else
         {
            $workingdir = \BF\IO\Path::Unixize( $workingdir );
         }
         if ( $workingdir == $owd )
         {
            $owd = null;
         }
         else
         {
            \chdir( $workingdir );
         }
         $sourceFile = \preg_replace(
            '~^' . \preg_quote( $workingdir, '~' ) . '/~',
            '',
            \BF\IO\Path::Unixize( $sourceFile )
         );
         $oldfile = null;
         if ( \file_exists( $zipFile ) )
         {
            $oldfile = $zipFile . '.old';
            try { self::Move( $zipFile, $oldfile ); }
            catch ( \Exception $ex )
            {
               if ( !\is_null( $owd ) )
               {
                  \chdir( $owd );
               }
               throw $ex;
            }
         }
         $zip = new \ZipArchive();
         if ( true === ( $res = $zip->open( $zipFile, \ZipArchive::CREATE ) ) )
         {
            $zip->addFile( $sourceFile );
            $zip->setArchiveComment( \BF\_( '_IO', 'Archived Single-File' ) );
            if ( ! $zip->close() )
            {
               if ( ! empty( $oldfile ) )
               {
                  self::Move( $oldfile, $zipFile );
               }
               if ( ! \is_null( $owd ) )
               {
                  \chdir( $owd );
               }
               throw new \BF\IO\FileAccessException(
                  $zipFile,
                  \BF\IO\FileAccessException::ACCESS_CREATE,
                  'Zipfile could not be created cause write could not be completed! (Closing file fails)'
               );
            }
            if ( ! empty( $oldfile ) )
            {
               self::Delete( $oldfile );
            }
            if ( ! \is_null( $owd ) )
            {
               \chdir( $owd );
            }
         }
         else
         {
            if ( ! \is_null( $owd ) )
            {
               \chdir( $owd );
            }
            if ( ! empty( $oldfile ) )
            {
               self::Move( $oldfile, $zipFile );
            }
            throw new \BF\IO\FileAccessException(
               $zipFile,
               \BF\IO\FileAccessException::ACCESS_CREATE,
               'Zipfile could not be created cause ' . self::GetZipArchiveError( $res )
            );
         }
      }

      /**
       * Compresses multiple files in a ZIP archive.
       *
       * If $sourceFiles is a numerically indicated array the origin file name is used inside the file.
       * If $sourceFiles is a associative array, the keys representing the file names, used inside the
       * archive, and the values are the real paths to the existing files.
       *
       * @param  array  $sourcefiles   The files to compress
       * @param  string $zipFile       The target/destination ZIP file. (will be created or overwrite a existing)
       * @param  string $zipFolderName Optional folder name, used inside the ZIP file, if one is required.
       * @throws \BF\MissingExtensionException If the PHP ZIP extension is not loaded.
       * @throws \BF\IO\FileAccessException If creating the ZIP file fails.
       * @throws \BF\IO\Exception
       */
      public static function ZipList( array $sourcefiles, $zipFile, $zipFolderName = null )
      {
         if ( ! \class_exists( '\\ZipArchive' ) )
         {
            throw new \BF\MissingExtensionException(
               'Zip',
               '\BF\IO\File::ZipList() fails with no ZIP-Support.'
            );
         }
         $oldfile = null;
         if ( \file_exists( $zipFile ) )
         {
            $oldfile = $zipFile . '.old';
            self::Move( $zipFile, $oldfile );
         }
         $zip = new \ZipArchive();
         if ( true === ( $res = $zip->open( $zipFile, \ZipArchive::CREATE ) ) )
         {
            if ( ! empty( $zipFolderName ) )
            {
               $zip->addEmptyDir( $zipFolderName );
               $zipFolderName = \rtrim( $zipFolderName, '\\/' ) . '/';
               foreach ( $sourcefiles as $k => $v )
               {
                  $key = \is_numeric( $k ) ? \basename( $v ) : $k;
                  $zip->addFile( $v, $zipFolderName . '/' . $key );
               }
            }
            else
            {
               foreach ( $sourcefiles as $k=>$v )
               {
                  if ( \is_numeric( $k ) )
                  {
                     $zip->addFile( $v, \basename( $v ) );
                  }
                  else
                  {
                     $zip->addFile( $v, $k );
                  }
               }
            }
            if ( ! $zip->close() )
            {
               if ( ! empty( $oldfile ) )
               {
                  self::Move( $oldfile, $zipFile );
               }
               throw new \BF\IO\FileAccessException(
                  $zipFile,
                  \BF\IO\FileAccessException::ACCESS_CREATE,
                  'Zipfile could not be created cause write could not be completed! (Closing file fails)'
               );
            }
         }
         else
         {
            if ( ! empty( $oldfile ) )
            {
               self::Move( $oldfile, $zipFile );
            }
            throw new \BF\IO\FileAccessException(
               $zipFile,
               \BF\IO\FileAccessException::ACCESS_CREATE,
               'Zipfile could not be created cause ' . self::GetZipArchiveError( $res ) );
         }
      }

      /**
       * Extracts all files from defined ZIP file to defined target/destination faolder.
       *
       * @param  string $zipFile      The ZIP file path.
       * @param  string $targetFolder The target/destination folder where the extracted files should be located.
       * @param  bool   $clearTarget  Empty/clear the target/destination folder before?
       * @throws \BF\MissingExtensionException If the PHP ZIP extension is not loaded.
       * @throws \BF\IO\FileNotFoundException  If the ZIP file dont exists.
       * @throws \BF\IO\FileAccessException    If reading the ZIP file fails.
       * @throws \BF\IO\Exception
       */
      public static function UnZip( $zipFile, $targetFolder, $clearTarget=true )
      {
         if ( !\class_exists('ZipArchive') )
         {
            throw new \BF\MissingExtensionException(
               'Zip',
               '\BF\IO\File::UnZip() fails with no ZIP-Support.'
            );
         }
         if ( ! \file_exists( $zipFile ) )
         {
            throw new \BF\IO\FileNotFoundException(
               $zipFile,
               'Could not extract from defined archive file.'
            );
         }
         if ( $clearTarget )
         {
            \BF\IO\Folder::MoveContents( $targetFolder, $targetFolder . '-tmp', 0700, true );
         }
         $zip = new \ZipArchive();
         if ( true === ( $res = $zip->open( $zipFile ) ) )
         {
            $zip->extractTo( $targetFolder );
            $zip->close();
         }
         else
         {
            if ( ! empty( $clearTarget ) )
            {
               \BF\IO\Folder::Move( $targetFolder . '-tmp', $targetFolder, 0700, true );
            }
            throw new \BF\IO\FileAccessException(
               $zipFile,
               \BF\IO\FileAccessException::ACCESS_READ,
               'Could not read from zip file cause ' . self::GetZipArchiveError( $res )
            );
         }
      }

      /**
       * Extracts the file with the name $zippedFileName inside the archive file to defined target/destination file.
       *
       * If the target file exists, it will be overwritten.
       *
       * @param  string $zipFile              The ZIP file path.
       * @param  string $zippedFileName       The name of the file to extract, used inside the ZIP file.
       * @param  string $targetFile           The file path of the resulting extracted file.
       * @throws \BF\IO\FileNotFoundException If the ZIP file dont exists.
       * @throws \BF\IO\FileAccessException
       */
      public static function UnZipSingleFile( $zipFile, $zippedFileName, $targetFile )
      {
         if ( ! \file_exists( $zipFile ) )
         {
            throw new \BF\IO\FileNotFoundException (
               $zipFile,
               'Could not extract from defined archive file.'
            );
         }
         try
         {
            \file_put_contents(
               $targetFile,
               \file_get_contents( 'zip://' . $zipFile . '#' . $zippedFileName )
            );
         }
         catch ( \Exception $ex )
         {
            throw \BF\IO\FileAccessException::Read(
               $zipFile,
               'Could not extract from defined archive file.',
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * @access private
       * @internal
       * @ignore
       */
      public static function GetZipArchiveError( $code )
      {
         switch ( $code )
         {
            case \ZipArchive::ER_COMPNOTSUPP:
                return 'Zip-Component is not supported.';
            case \ZipArchive::ER_CRC:
                return 'a CRC-Error is occoure.';
            case \ZipArchive::ER_INCONS:
                return 'it results in a inconsistent zip-archive.';
            case \ZipArchive::ER_INTERNAL:
                return 'a internal error (unknown error) is thrown.';
            case \ZipArchive::ER_MEMORY:
                return 'zip-creation needs memory more than usable memorysize.';
            case \ZipArchive::ER_OPEN:
                return 'open the archive results in a error.';
            case \ZipArchive::ER_WRITE:
                return 'writing into archive results in a error.';
            case \ZipArchive::ER_ZLIB:
                return 'a ZLib error (unknown message) is thrown.';
            default:
                return 'a unknown error is thrown.';
         }
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="GetExtension + GetExtensionName + GetNameWithoutExtension + ChangeExtension">

      /**
       * Returns the file name extension, including the leading dot, for defined file name/path.
       *
       * @param  string  $file           The file name/path.
       * @param  boolean $doubleExtension  If you require extensions, including also a single dot like '.abc.def'
       *                                 you have to set this parameter to TRUE.
       * @return string|FALSE Returns the file name extension NOT including the leading dot or FALSE if not extension.
       */
      public static function GetExtension( $file, $doubleExtension = false )
      {
         // In case the path is a URL, strip any query string before getting extension
         $quoteCharPosition = \strpos( $file, '?' );
         if ( false !== $quoteCharPosition )
         {
            $file = \substr( $file, 0, $quoteCharPosition );
         }
         if ( ! $doubleExtension )
         {
            return '.' . \ltrim( \BF\IO\Path::GetPathinfo( $file, \PATHINFO_EXTENSION ), '.' );
         }
         $hits = null;
         $file = \basename( $file );
         if ( \preg_match( '~^.+(\.[a-z0-9]{1,6}\.[a-z0-9]{1,6})$~i', $file, $hits ) )
         {
            return $hits[ 1 ];
         }
         $tmp = \explode( '.', $file );
         $tsz = \count( $tmp );
         if ( $tsz < 2 )
         {
            return false;
         }
         return '.' . $tmp[ $tsz - 1 ];
      }

      /**
       * Returns the file name extension, NOT including the leading dot, for defined file name/path.
       *
       * @param  string  $file           The file name/path.
       * @param  boolean $doubleExtension  If you require extensions, including also a single dot like '.abc.def'
       *                                 you have to set this parameter to TRUE.
       * @return string|FALSE Returns the file name extension NOT including the leading dot or FALSE if not extension.
       */
      public static function GetExtensionName( $file, $doubleExtension = false )
      {
         return \ltrim( self::GetExtension( $file, $doubleExtension ), '.' );
      }

      /**
       * Returns the file name without the file name extension.
       *
       * @param  string  $file           The file name/path.
       * @param  boolean $doubleExtension  If you require extensions, including also a single dot like '.abc.def'
       *                                 you have to set this parameter to TRUE.
       * @return string|bool Name or bool FALSE.
       */
      public static function GetNameWithoutExtension( $file, $doubleExtension = false )
      {
         if ( \FALSE === ( $ext = self::GetExtension( $file, $doubleExtension ) ) )
         {
            return \basename( $file );
         }
         return \substr( \basename( $file ), 0, -\strlen( $ext ) );
      }

      /**
       * Changes the file name extension of a defined file name/path. If the file exists in current file system
       * and $handle is TRUE, it will be renamed also for real.
       *
       * @param  string  $file           The file name/path.
       * @param  string  $newExtension   The new file name extension (with or without the leading dot)
       * @param  boolean $doubleExtension  If you require extensions, including also a single dot like '.abc.def'
       *                                 you have to set this parameter to TRUE.
       * @param  boolean $handle         Also real rename the file from file system if it exists? !!!Handle with care!!!
       * @return string
       * @throws \BF\IO\Exception
       */
      public static function ChangeExtension( $file, $newExtension, $doubleExtension = false, $handle = false )
      {
         $base = \basename( $file );
         $folder = \substr( $file, 0, -\strlen( $base ) );
         $newExtension = '.' . \ltrim( $newExtension, '.' );
         $result =
               $folder
            .  self::GetNameWithoutExtension( $file, $doubleExtension )
            .  $newExtension;
         if ( $handle && \file_exists( $file ) )
         {
            self::Move( $file, $result, true );
         }
         return $result;
      }

      # </editor-fold>

      # </editor-fold>

   }

}

